import { _removeFromArray } from '../agStack/utils/array';
import { BeanStub } from '../context/beanStub';
import type { BeanCollection } from '../context/context';
import type { AgColumn } from '../entities/agColumn';
import type { ColKey } from '../entities/colDef';
import type { ColumnEvent, ColumnEventType } from '../events';
import type { IAggFuncService } from '../interfaces/iAggFuncService';
import type {
    ColumnExtractors,
    ColumnOrdering,
    ColumnProcessor,
    ColumnProcessors,
    IColsService,
} from '../interfaces/iColsService';
import type { WithoutGridCommon } from '../interfaces/iCommon';
import type { IGroupHierarchyColService } from '../interfaces/iGroupHierarchyColService';
import type { ColumnChangedEventType } from './columnApi';
import { dispatchColumnChangedEvent } from './columnEventUtils';
import type { ColumnModel, Maybe } from './columnModel';
import type { ColumnState, ColumnStateParams } from './columnStateUtils';
import type { VisibleColsService } from './visibleColsService';

export abstract class BaseColsService extends BeanStub implements IColsService {
    protected colModel: ColumnModel;
    protected aggFuncSvc?: IAggFuncService;
    protected visibleCols: VisibleColsService;
    protected groupHierarchCols?: IGroupHierarchyColService;
    protected dispatchColumnChangedEvent = dispatchColumnChangedEvent;

    abstract eventName: ColumnChangedEventType;
    abstract columnProcessors: ColumnProcessors;
    abstract columnExtractors: ColumnExtractors;
    columnOrdering: ColumnOrdering;

    public columns: AgColumn[] = [];
    public columnIndexMap: { [key: string]: number } = {};

    public wireBeans(beans: BeanCollection): void {
        this.colModel = beans.colModel;
        this.aggFuncSvc = beans.aggFuncSvc;
        this.visibleCols = beans.visibleCols;
        this.groupHierarchCols = beans.groupHierarchyColSvc;
    }

    public sortColumns(compareFn: (a: AgColumn, b: AgColumn) => number): void {
        const { groupHierarchCols } = this;
        this.columns.sort((a, b) => groupHierarchCols?.compareVirtualColumns(a, b) ?? compareFn(a, b));
        this.updateIndexMap();
    }

    public setColumns(colKeys: ColKey[] | undefined, source: ColumnEventType): void {
        this.setColList(colKeys, this.columns, this.eventName, true, true, this.columnProcessors.set, source);
    }

    public addColumns(colKeys: ColKey[] | undefined, source: ColumnEventType): void {
        this.updateColList(colKeys, this.columns, true, true, this.columnProcessors.add, this.eventName, source);
    }

    public removeColumns(colKeys: ColKey[] | undefined, source: ColumnEventType): void {
        this.updateColList(colKeys, this.columns, false, true, this.columnProcessors.remove, this.eventName, source);
    }

    public getColumnIndex(colId: string): number | undefined {
        return this.columnIndexMap[colId];
    }

    protected updateIndexMap = (): void => {
        this.columnIndexMap = {};
        this.columns.forEach((col, index) => (this.columnIndexMap[col.getId()] = index));
    };

    private setColList(
        colKeys: ColKey[] = [],
        masterList: AgColumn[],
        eventName: IColsService['eventName'],
        detectOrderChange: boolean,
        autoGroupsNeedBuilding: boolean,
        columnCallback: ColumnProcessor,
        source: ColumnEventType
    ): void {
        const gridColumns = this.colModel.getCols();
        if (!gridColumns || gridColumns.length === 0) {
            return;
        }

        const changes: Map<AgColumn, number> = new Map();
        // store all original cols and their index.
        masterList.forEach((col, idx) => changes.set(col, idx));

        masterList.length = 0;

        for (const key of colKeys) {
            const column = this.colModel.getColDefCol(key);
            if (column) {
                masterList.push(column);
            }
        }

        masterList.forEach((col, idx) => {
            const oldIndex = changes.get(col);
            // if the column was not in the list, we add it as it's a change
            // idx is irrelevant now.
            if (oldIndex === undefined) {
                changes.set(col, 0);
                return;
            }

            if (detectOrderChange && oldIndex !== idx) {
                // if we're detecting order changes, and the indexes differ, we retain this as it's changed
                return;
            }

            // otherwise remove this col, as it's unchanged.
            changes.delete(col);
        });

        this.updateIndexMap();

        const primaryCols = this.colModel.getColDefCols();

        for (const column of primaryCols ?? []) {
            const added = masterList.indexOf(column) >= 0;
            columnCallback(column, added, source);
        }

        autoGroupsNeedBuilding && this.colModel.refreshCols(false, source);

        this.visibleCols.refresh(source);

        this.dispatchColumnChangedEvent(this.eventSvc, eventName, [...changes.keys()], source);
    }

    private updateColList(
        keys: Maybe<ColKey>[] = [],
        masterList: AgColumn[],
        actionIsAdd: boolean,
        autoGroupsNeedBuilding: boolean,
        columnCallback: ColumnProcessor,
        eventType: IColsService['eventName'],
        source: ColumnEventType
    ) {
        if (!keys || keys.length === 0) {
            return;
        }

        let atLeastOne = false;
        const updatedCols: Set<AgColumn> = new Set();

        for (const key of keys) {
            if (!key) {
                continue;
            }
            const columnToAdd = this.colModel.getColDefCol(key);
            if (!columnToAdd) {
                continue;
            }
            updatedCols.add(columnToAdd);

            if (actionIsAdd) {
                if (masterList.indexOf(columnToAdd) >= 0) {
                    continue;
                }
                masterList.push(columnToAdd);
            } else {
                const currentIndex = masterList.indexOf(columnToAdd);
                if (currentIndex < 0) {
                    continue;
                }
                for (let i = currentIndex + 1; i < masterList.length; i++) {
                    // row indexes of subsequent columns have changed
                    updatedCols.add(masterList[i]);
                }
                _removeFromArray(masterList, columnToAdd);
            }

            columnCallback(columnToAdd, actionIsAdd, source);
            atLeastOne = true;
        }

        if (!atLeastOne) {
            return;
        }

        this.updateIndexMap();

        if (autoGroupsNeedBuilding) {
            this.colModel.refreshCols(false, source);
        }

        this.visibleCols.refresh(source);

        const eventColumns = Array.from(updatedCols);
        this.eventSvc.dispatchEvent({
            type: eventType,
            columns: eventColumns,
            column: eventColumns.length === 1 ? eventColumns[0] : null,
            source,
        } as WithoutGridCommon<ColumnEvent>);
    }

    public extractCols(source: ColumnEventType, oldProvidedCols: AgColumn[] = []): AgColumn[] {
        const previousCols = this.columns;
        const colsWithIndex: AgColumn[] = [];
        const colsWithValue: AgColumn[] = [];

        const { setFlagFunc, getIndexFunc, getInitialIndexFunc, getValueFunc, getInitialValueFunc } =
            this.columnExtractors;

        const primaryCols = this.colModel.getColDefCols();

        // go though all cols.
        // if value, change
        // if default only, change only if new
        for (const col of primaryCols ?? []) {
            const colIsNew = !oldProvidedCols.includes(col);
            const colDef = col.getColDef();

            const value = getValueFunc(colDef);
            const initialValue = getInitialValueFunc(colDef);
            const index = getIndexFunc(colDef);
            const initialIndex = getInitialIndexFunc(colDef);

            let include: boolean;

            const valuePresent = value !== undefined;
            const indexPresent = index !== undefined;
            const initialValuePresent = initialValue !== undefined;
            const initialIndexPresent = initialIndex !== undefined;

            if (valuePresent) {
                include = value!; // boolean value is guaranteed as attrToBoolean() is used above
            } else if (indexPresent) {
                if (index === null) {
                    // if col is new we don't want to use the default / initial if index is set to null. Similarly,
                    // we don't want to include the property for existing columns, i.e. we want to 'clear' it.
                    include = false;
                } else {
                    // note that 'null >= 0' evaluates to true which means 'rowGroupIndex = null' would enable row
                    // grouping if the null check didn't exist above.
                    include = index >= 0;
                }
            } else if (colIsNew) {
                // as no value or index is 'present' we use the default / initial when col is new
                if (initialValuePresent) {
                    include = initialValue!;
                } else if (initialIndexPresent) {
                    include = initialIndex != null && initialIndex >= 0;
                } else {
                    include = false;
                }
            } else {
                // otherwise include it if included last time, e.g. if we are extracting row group cols and this col
                // is an existing row group col (i.e. it exists in 'previousCols') then we should include it.
                include = previousCols.indexOf(col) >= 0;
            }

            if (include) {
                const useIndex = colIsNew ? index != null || initialIndex != null : index != null;
                useIndex ? colsWithIndex.push(col) : colsWithValue.push(col);
            }
        }

        const getIndexForCol = (col: AgColumn): number => {
            const colDef = col.getColDef();
            return getIndexFunc(colDef) ?? getInitialIndexFunc(colDef)!;
        };

        // sort cols with index, and add these first
        colsWithIndex.sort((colA, colB) => getIndexForCol(colA) - getIndexForCol(colB));

        const res: AgColumn[] = [];

        const groupHierarchCols = this.groupHierarchCols;
        const addCol = (col: AgColumn) => {
            if (groupHierarchCols) {
                groupHierarchCols.expandColumnInto(res, col);
            } else {
                res.push(col);
            }
        };

        // Columns with an index specified need to have any virtual hierarchical columns expanded
        colsWithIndex.forEach(addCol);

        // next, add columns that were there before and in the same order as they were before,
        // so we are preserving order of current grouping of columns that simply have rowGroup=true...
        for (const col of previousCols) {
            if (colsWithValue.indexOf(col) >= 0) {
                // ...with the caveat that each column added also has any associated virtual columns added here
                // so they appear before it in the group hierarchy. This is purely a matter of ordering; adding the
                // virtual columns here means they will not be added below when iterating over `colsWithValue`.
                addCol(col);
            }
        }

        // lastly put in all remaining cols
        for (const col of colsWithValue) {
            if (res.indexOf(col) < 0) {
                addCol(col);
            }
        }

        // set flag=false for removed cols
        for (const col of previousCols) {
            if (res.indexOf(col) < 0) {
                setFlagFunc(col, false, source);
            }
        }
        // set flag=true for newly added cols
        for (const col of res) {
            if (previousCols.indexOf(col) < 0) {
                setFlagFunc(col, true, source);
            }
        }

        this.columns = res;
        this.updateIndexMap();
        return this.columns;
    }

    public abstract syncColumnWithState(
        column: AgColumn,
        source: ColumnEventType,
        getValue: <U extends keyof ColumnStateParams, S extends keyof ColumnStateParams>(
            key1: U,
            key2?: S
        ) => { value1: ColumnStateParams[U] | undefined; value2: ColumnStateParams[S] | undefined },
        rowIndex: { [key: string]: number } | null
    ): void;

    public restoreColumnOrder(
        columnStateAccumulator: { [colId: string]: ColumnState },
        incomingColumnState: { [colId: string]: ColumnState }
    ): { [colId: string]: ColumnState } {
        const colList = this.columns;

        const primaryCols = this.colModel.getColDefCols();
        if (!colList.length || !primaryCols) {
            return columnStateAccumulator;
        }
        const updatedColIdArray = Object.keys(incomingColumnState);
        const updatedColIds = new Set(updatedColIdArray);
        const newColIds = new Set(updatedColIdArray);
        const allColIds = new Set(
            colList
                .map((column) => {
                    const colId = column.getColId();
                    newColIds.delete(colId);
                    return colId;
                })
                .concat(updatedColIdArray)
        );

        const colIdsInOriginalOrder: string[] = [];
        const originalOrderMap: { [colId: string]: number } = {};
        let orderIndex = 0;
        for (let i = 0; i < primaryCols.length; i++) {
            const colId = primaryCols[i].getColId();
            if (allColIds.has(colId)) {
                colIdsInOriginalOrder.push(colId);
                originalOrderMap[colId] = orderIndex++;
            }
        }

        // follow approach in `resetColumnState`
        let index = 1000;
        let hasAddedNewCols = false;
        let lastIndex = 0;

        const enableProp = this.columnOrdering.enableProp;
        const initialEnableProp = this.columnOrdering.initialEnableProp;
        const indexProp = this.columnOrdering.indexProp;
        const initialIndexProp = this.columnOrdering.initialIndexProp;

        const processPrecedingNewCols = (colId: string) => {
            const originalOrderIndex = originalOrderMap[colId];
            for (let i = lastIndex; i < originalOrderIndex; i++) {
                const newColId = colIdsInOriginalOrder[i];
                if (newColIds.has(newColId)) {
                    incomingColumnState[newColId][indexProp] = index++;
                    newColIds.delete(newColId);
                }
            }
            lastIndex = originalOrderIndex;
        };

        for (const column of colList) {
            const colId = column.getColId();
            if (updatedColIds.has(colId)) {
                // New col already exists. Add any other new cols that should be before it.
                processPrecedingNewCols(colId);
                incomingColumnState[colId][indexProp] = index++;
            } else {
                const colDef = column.getColDef();
                const missingIndex =
                    colDef[indexProp] === null || (colDef[indexProp] === undefined && colDef[initialIndexProp] == null);
                if (missingIndex) {
                    if (!hasAddedNewCols) {
                        const propEnabled =
                            colDef[enableProp] || (colDef[enableProp] === undefined && colDef[initialEnableProp]);
                        if (propEnabled) {
                            processPrecedingNewCols(colId);
                        } else {
                            // Reached the first manually added column. Add all the new columns now.
                            for (const newColId of newColIds) {
                                // Rather than increment the index, just use the original order index - doesn't need to be contiguous.
                                incomingColumnState[newColId][indexProp] = index + originalOrderMap[newColId];
                            }
                            index += colIdsInOriginalOrder.length;
                            hasAddedNewCols = true;
                        }
                    }
                    if (!columnStateAccumulator[colId]) {
                        columnStateAccumulator[colId] = { colId };
                    }
                    columnStateAccumulator[colId][indexProp] = index++;
                }
            }
        }

        return columnStateAccumulator;
    }
}
